package com.gitee.cashzhang27.test.cloud.oauth.auth.server.extended.provider;

import com.gitee.cashzhang27.test.cloud.oauth.auth.server.extended.entity.DictCodeEnums;
import com.gitee.cashzhang27.test.cloud.oauth.auth.server.extended.service.AuthUserDetailsService;
import com.gitee.cashzhang27.test.cloud.oauth.auth.server.extended.token.PhoneNumberVerifyCodeAuthenticationToken;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.Assert;

/**
 * An {@link AuthenticationProvider} implementation that retrieves user details from a {@link
 * AuthUserDetailsService}.
 *
 * @author Cash Zhang
 * @version v1.0
 * @since 2019/02/21 14:31
 */
@Slf4j
public class PhoneAuthenticationProvider extends
    AbstractUserDetailsAuthenticationProviderByPhone {

  // ~ Static fields/initializers
  // =====================================================================================

  /**
   * The plaintext password used to perform PasswordEncoder#matches(CharSequence, String)}  on when
   * the user is not found to avoid SEC-2056.
   */
  private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";

  // ~ Instance fields
  // ================================================================================================

  private PasswordEncoder passwordEncoder;

  /**
   * The password used to perform {@link PasswordEncoder#matches(CharSequence, String)} on when the
   * user is not found to avoid SEC-2056. This is necessary, because some {@link PasswordEncoder}
   * implementations will short circuit if the password is not in a valid format.
   */
  private volatile String userNotFoundEncodedPassword;

  private AuthUserDetailsService authUserDetailsService;

  public PhoneAuthenticationProvider() {
    setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
  }

  // ~ Methods
  // ========================================================================================================

  @Override
  protected void additionalAuthenticationChecks(UserDetails userDetails,
      PhoneNumberVerifyCodeAuthenticationToken authentication)
      throws AuthenticationException {
    if (authentication.getCredentials() == null) {
      log.debug("Authentication failed: no credentials provided");

      throw new BadCredentialsException(messages.getMessage(
          "AbstractUserDetailsAuthenticationProvider.badCredentials",
          "Bad credentials"));
    }

    String presentedPassword = authentication.getCredentials().toString();

    if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
      log.debug("Authentication failed: password does not match stored value");

      throw new BadCredentialsException(messages.getMessage(
          "AbstractUserDetailsAuthenticationProvider.badCredentials",
          "Bad credentials"));
    }
  }

  @Override
  protected void doAfterPropertiesSet() throws Exception {
    Assert.notNull(this.authUserDetailsService, "A UserDetailsService must be set");
  }

  @Override
  protected final UserDetails retrieveUser(String phoneNumber,
      PhoneNumberVerifyCodeAuthenticationToken authentication)
      throws AuthenticationException {
    prepareTimingAttackProtection();
    try {
      DictCodeEnums userType = authentication.getUserType();
      UserDetails loadedUser = this.getUserDetailsService()
          .loadUserByPhoneNumber(phoneNumber, userType);
      if (loadedUser == null) {
        throw new InternalAuthenticationServiceException(
            "UserDetailsService returned null, which is an interface contract violation");
      }
      return loadedUser;
    } catch (UsernameNotFoundException ex) {
      mitigateAgainstTimingAttack(authentication);
      throw ex;
    } catch (InternalAuthenticationServiceException ex) {
      throw ex;
    } catch (Exception ex) {
      throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
    }
  }

  private void prepareTimingAttackProtection() {
    if (this.userNotFoundEncodedPassword == null) {
      this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
    }
  }

  private void mitigateAgainstTimingAttack(Authentication authentication) {
    if (authentication.getCredentials() != null) {
      String presentedPassword = authentication.getCredentials().toString();
      this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
    }
  }

  /**
   * Sets the PasswordEncoder instance to be used to encode and validate passwords. If not set, the
   * password will be compared using {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}
   *
   * @param passwordEncoder must be an instance of one of the {@code PasswordEncoder} types.
   */
  public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
    Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
    this.passwordEncoder = passwordEncoder;
    this.userNotFoundEncodedPassword = null;
  }

  protected PasswordEncoder getPasswordEncoder() {
    return passwordEncoder;
  }

  public void setUserDetailsService(AuthUserDetailsService userDetailsService) {
    this.authUserDetailsService = userDetailsService;
  }

  protected AuthUserDetailsService getUserDetailsService() {
    return authUserDetailsService;
  }
}
